/**
 * (created at 2011-1-28)
 */
package qiushuo.mysqlparser.recognizer.parser;

import static qiushuo.mysqlparser.recognizer.SQLTokenType.IDENTIFIER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_ALL;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_BY;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_DESC;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_DISTINCT;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_FOR;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_IN;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_LIMIT;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_LOCK;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_UPDATE;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.OP_ASTERISK;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_COMMA;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_LEFT_PAREN;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_RIGHT_PAREN;

import java.sql.SQLSyntaxErrorException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import qiushuo.mysqlparser.ast.expression.Expression;
import qiushuo.mysqlparser.ast.expression.primary.Wildcard;
import qiushuo.mysqlparser.ast.fragment.GroupBy;
import qiushuo.mysqlparser.ast.fragment.Limit;
import qiushuo.mysqlparser.ast.fragment.OrderBy;
import qiushuo.mysqlparser.ast.fragment.tableref.TableReferences;
import qiushuo.mysqlparser.ast.statement.DMLSelectStatement;
import qiushuo.mysqlparser.ast.statement.DMLSelectUnionStatement;
import qiushuo.mysqlparser.recognizer.SQLToken;
import qiushuo.mysqlparser.recognizer.TokenReader;
import qiushuo.mysqlparser.util.Pair;

/**
 * @author <a href="mailto:QiuShuo1985@gmail.com">QIU Shuo</a>
 */
public class DMLSelectStmtParser extends DMLParser {
    public DMLSelectStmtParser(TokenReader input) {
        super(input);
    }

    /**
     * no token has been consumed
     */
    public DMLSelectUnionStatement selectUnionStmt() throws SQLSyntaxErrorException {
        List<DMLSelectStatement> list = new LinkedList<DMLSelectStatement>();
        boolean isAllSelectInParen = true;
        boolean hasUnion = false;
        boolean unionAll = true;
        OrderBy orderBy = null;
        Limit limit = null;
        while (true) {
            DMLSelectStatement select = null;
            if (input.isNext(PUNC_LEFT_PAREN, 0)) {
                select = selectStmt();
                match(PUNC_RIGHT_PAREN);
            } else {
                isAllSelectInParen = false;
                select = selectStmt();
            }
            list.add(select);
            SQLToken token = input.next();
            if (token == null) {
                return new DMLSelectUnionStatement(list, unionAll, null, null);
            }
            switch (token.getType()) {
            case KW_UNION:
                if (input.isNext(KW_ALL, 0)) {
                    input.skipForce(1);
                } else if (input.isNext(KW_DISTINCT, 0)) {
                    unionAll = false;
                    input.skipForce(1);
                } else {
                    unionAll = false;
                }
                hasUnion = true;
                continue;
            case KW_ORDER:
                if (input.isNext(KW_BY, 0)) {
                    input.skipForce(1);
                } else {
                    input.backForce();
                    return new DMLSelectUnionStatement(list, unionAll, null, null);
                }
                if (!hasUnion) {
                    throw err("unexpect ORDER BY: no UNION");
                }
                if (!isAllSelectInParen) {
                    throw err("all select for UNION must be included in '(' ')' if ORDER BY or LIMIT of UNION is engaged");
                }
                orderBy = orderBy();
                if (!input.isNext(KW_LIMIT, 0)) {
                    return new DMLSelectUnionStatement(list, unionAll, orderBy, null);
                }
                input.skipForce(1);
            case KW_LIMIT:
                if (!hasUnion) {
                    throw err("unexpect LIMIT: no UNION");
                }
                if (!isAllSelectInParen) {
                    throw err("all select for UNION must be included in '(' ')' if ORDER BY or LIMIT of UNION is engaged");
                }
                limit = limit();
                return new DMLSelectUnionStatement(list, unionAll, orderBy, limit);
            default:
                input.backForce();
                return new DMLSelectUnionStatement(list, unionAll, orderBy, limit);
            }
        }
    }

    /**
     * 锟斤拷锟斤拷union锟斤拷select锟斤拷 no token has been pre-consumed.
     * <code>((((select ...))))</code> is OK.
     * union锟斤拷锟斤拷嵌锟阶ｏ拷锟斤拷为union锟斤拷锟杰憋拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷
     */
    private DMLSelectStatement selectStmt() throws SQLSyntaxErrorException {
        SQLToken token = input.next();
        if (token == null) {
            err("unexpected EOF for SELECT");
        }
        DMLSelectStatement select;
        switch (token.getType()) {
        case PUNC_LEFT_PAREN:
            select = selectStmt();
            match(PUNC_RIGHT_PAREN);
            return select;
        case KW_SELECT:
            break;
        default:
            throw err("unexpected token for SELECT: " + token);
        }
        Pair<int[], boolean[]> opts = selectOpts();
        SelectStep selectStep = new SelectStep();
        List<Pair<Expression, String>> exprList = selectExprList();
        selectStep.updateStep(input.next());
        TableReferences tables = null;
        Expression where = null;
        GroupBy group = null;
        Expression having = null;
        OrderBy order = null;
        Limit limit = null;
        if (selectStep.isFrom()) {
            tables = tableRefs();
            selectStep.updateStep(input.next());
        }
        if (selectStep.isWhere()) {
            where = exprParser.expression();
            selectStep.updateStep(input.next());
        }
        if (selectStep.isGroupBy()) {
            group = groupBy();
            selectStep.updateStep(input.next());
        }
        if (selectStep.isHaving()) {
            having = exprParser.expression();
            selectStep.updateStep(input.next());
        }
        if (selectStep.isOrderBy()) {
            order = orderBy();
            selectStep.updateStep(input.next());
        }
        if (selectStep.isLimit()) {
            limit = limit();
            selectStep.updateStep(input.next());
        }
        int lockMode = DMLSelectStatement.LOCK_UNDEF;
        if (input.isNext(KW_FOR, 0) && input.isNext(KW_UPDATE, 1)) {
            input.skipForce(2);
            lockMode = DMLSelectStatement.LOCK_FOR_UPDATE;
        } else if (input.isNext(KW_LOCK, 0)
                   && input.isNext(KW_IN, 1)
                   && input.isNext(IDENTIFIER, 2, true, "SHARE")
                   && input.isNext(IDENTIFIER, 3, true, "MODE")) {
            input.skipForce(4);
            lockMode = DMLSelectStatement.LOCK_SHARED;
        }
        return new DMLSelectStatement(opts, exprList, tables, where, group, having, order, limit, lockMode);
    }

    private class SelectStep {
        private static final int SELECT_STEP_FROM = 1;
        private static final int SELECT_STEP_WHERE = 2;
        private static final int SELECT_STEP_GROUP_BY = 3;
        private static final int SELECT_STEP_HAVING = 4;
        private static final int SELECT_STEP_ORDER_BY = 5;
        private static final int SELECT_STEP_LIMIT = 6;
        private static final int SELECT_STEP_END = Integer.MAX_VALUE;
        public int selectStep = SELECT_STEP_FROM;

        public boolean isFrom() {
            return selectStep <= SELECT_STEP_FROM;
        }

        public boolean isWhere() {
            return selectStep <= SELECT_STEP_WHERE;
        }

        public boolean isGroupBy() {
            return selectStep <= SELECT_STEP_GROUP_BY;
        }

        public boolean isHaving() {
            return selectStep <= SELECT_STEP_HAVING;
        }

        public boolean isOrderBy() {
            return selectStep <= SELECT_STEP_ORDER_BY;
        }

        public boolean isLimit() {
            return selectStep <= SELECT_STEP_LIMIT;
        }

        /**
         * if don't match, input will be back
         * 
         * @param this token is already consumed
         * @throws SQLSyntaxErrorException
         */
        public void updateStep(SQLToken token) throws SQLSyntaxErrorException {
            if (token == null) {
                selectStep = SELECT_STEP_END;
                return;
            }
            switch (token.getType()) {
            case KW_FROM:
                selectStep = SELECT_STEP_FROM;
                return;
            case KW_WHERE:
                selectStep = SELECT_STEP_WHERE;
                return;
            case KW_GROUP:
                DMLSelectStmtParser.this.match(KW_BY);
                selectStep = SELECT_STEP_GROUP_BY;
                return;
            case KW_HAVING:
                selectStep = SELECT_STEP_HAVING;
                return;
            case KW_ORDER:
                DMLSelectStmtParser.this.match(KW_BY);
                selectStep = SELECT_STEP_ORDER_BY;
                return;
            case KW_LIMIT:
                selectStep = SELECT_STEP_LIMIT;
                return;
            default:
                input.backForce();
                selectStep = SELECT_STEP_END;
                return;
            }
        }
    }

    private GroupBy groupBy() throws SQLSyntaxErrorException {
        List<Pair<Expression, Boolean>> eles = new LinkedList<Pair<Expression, Boolean>>();
        while (true) {
            Expression expr = exprParser.expression();
            SQLToken token = input.next();
            if (token == null) {
                eles.add(new Pair<Expression, Boolean>(expr, false));
                return new GroupBy(eles, false);
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                eles.add(new Pair<Expression, Boolean>(expr, false));
                continue;
            case KW_ASC:
            case KW_DESC:
                eles.add(new Pair<Expression, Boolean>(expr, KW_DESC.equals(token.getType())));
                token = input.next();
                if (token == null) {
                    return new GroupBy(eles, false);
                }
                switch (token.getType()) {
                case PUNC_COMMA:
                    continue;
                case KW_WITH:
                    if (input.isNext(IDENTIFIER, 0, true, "ROLLUP")) {
                        input.skipForce(1);
                        return new GroupBy(eles, true);
                    }
                default:
                    input.backForce();
                    return new GroupBy(eles, false);
                }
            case KW_WITH:
                if (input.isNext(IDENTIFIER, 0, true, "ROLLUP")) {
                    input.skipForce(1);
                    return new GroupBy(eles, true);
                }
            default:
                input.backForce();
                return new GroupBy(eles, false);
            }
        }
    }

    private List<Pair<Expression, String>> selectExprList() throws SQLSyntaxErrorException {
        List<Pair<Expression, String>> list = new LinkedList<Pair<Expression, String>>();
        SQLToken token = null;
        while (true) {
            Expression item = null;
            boolean wildcard = input.isNext(OP_ASTERISK, 0);
            if (wildcard) {
                input.skipForce(1);
                item = new Wildcard();
            } else {
                item = exprParser.expression();
            }
            token = input.next();
            if (token == null) {
                list.add(new Pair<Expression, String>(item, null));
                return list;
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                list.add(new Pair<Expression, String>(item, null));
                continue;
            case KW_AS:
                token = input.next();
                if (token == null) err("EOF after AS for SELECT");
            case IDENTIFIER:
            case LITERAL_STRING:
                if (wildcard) err("alias is forbidden for *");
                token = input.next();
                if (PUNC_COMMA.equals(token.getType())) {
                    list.add(new Pair<Expression, String>(item, token.getText()));
                    continue;
                }
            default:
                input.backForce();
                list.add(new Pair<Expression, String>(item, null));
                return list;
            }
        }
    }

    private Pair<int[], boolean[]> selectOpts() throws SQLSyntaxErrorException {
        int dupStrategy = DMLSelectStatement.DUP_STRATEGY_UNDEF;
        boolean highPriority = false;
        boolean straightJoin = false;
        boolean sqlSmallResult = false;
        boolean sqlBigResult = false;
        boolean sqlBufferResult = false;
        int sqlCache = DMLSelectStatement.SQL_CACHE_UNDEF;
        boolean sqlCalcFoundRows = false;
        loop1: while (true) {
            SQLToken t = input.next();
            if (t == null) {
                break;
            }
            switch (t.getType()) {
            case KW_ALL:
                if (dupStrategy != DMLSelectStatement.DUP_STRATEGY_UNDEF) err("duplicated opts for SELECT: " + t);
                dupStrategy = DMLSelectStatement.DUP_STRATEGY_ALL;
                continue;
            case KW_DISTINCT:
                if (dupStrategy != DMLSelectStatement.DUP_STRATEGY_UNDEF) err("duplicated opts for SELECT: " + t);
                dupStrategy = DMLSelectStatement.DUP_STRATEGY_DISTINCT;
                continue;
            case KW_DISTINCTROW:
                if (dupStrategy != DMLSelectStatement.DUP_STRATEGY_UNDEF) err("duplicated opts for SELECT: " + t);
                dupStrategy = DMLSelectStatement.DUP_STRATEGY_DISTINCTROW;
                continue;
            case KW_HIGH_PRIORITY:
                if (highPriority) err("duplicated opts for SELECT: " + t);
                highPriority = true;
                continue;
            case KW_STRAIGHT_JOIN:
                if (straightJoin) err("duplicated opts for SELECT: " + t);
                straightJoin = true;
                continue;
            case KW_SQL_SMALL_RESULT:
                if (sqlSmallResult) err("duplicated opts for SELECT: " + t);
                sqlSmallResult = true;
                continue;
            case KW_SQL_BIG_RESULT:
                if (sqlBigResult) err("duplicated opts for SELECT: " + t);
                sqlBigResult = true;
                continue;
            case KW_SQL_CALC_FOUND_ROWS:
                if (sqlCalcFoundRows) err("duplicated opts for SELECT: " + t);
                sqlCalcFoundRows = true;
                continue;
            case IDENTIFIER:
                Integer optsSwitch = SELECT_OPTS_ID_MAP.get(t.getText().toUpperCase());
                if (optsSwitch != null) {
                    switch (SELECT_OPTS_ID_MAP.get(optsSwitch)) {
                    case SELECT_OPTS_ID_SQL_BUFFER_RESULT:
                        if (sqlBufferResult) err("duplicated opts for SELECT: " + t);
                        sqlBufferResult = true;
                        continue;
                    case SELECT_OPTS_ID_SQL_CACHE:
                        if (sqlCache != DMLSelectStatement.SQL_CACHE_UNDEF) err("duplicated opts for SELECT: " + t);
                        sqlCache = DMLSelectStatement.SQL_CACHE_CACHE;
                        continue;
                    case SELECT_OPTS_ID_SQL_NO_CACHE:
                        if (sqlCache != DMLSelectStatement.SQL_CACHE_UNDEF) err("duplicated opts for SELECT: " + t);
                        sqlCache = DMLSelectStatement.SQL_CACHE_NO_CACHE;
                        continue;
                    }
                }
            default:
                input.backForce();
                break loop1;
            }
        }
        return new Pair<int[], boolean[]>(new int[] { dupStrategy, sqlCache }, new boolean[] { highPriority,
                straightJoin, sqlSmallResult, sqlBigResult, sqlBufferResult, sqlCalcFoundRows });
    }

    private static final Map<String, Integer> SELECT_OPTS_ID_MAP = new HashMap<String, Integer>(3);
    private static final int SELECT_OPTS_ID_SQL_BUFFER_RESULT = 1;
    private static final int SELECT_OPTS_ID_SQL_CACHE = 2;
    private static final int SELECT_OPTS_ID_SQL_NO_CACHE = 3;
    static {
        SELECT_OPTS_ID_MAP.put("SQL_BUFFER_RESULT", SELECT_OPTS_ID_SQL_BUFFER_RESULT);
        SELECT_OPTS_ID_MAP.put("SQL_CACHE", SELECT_OPTS_ID_SQL_CACHE);
        SELECT_OPTS_ID_MAP.put("SQL_NO_CACHE", SELECT_OPTS_ID_SQL_NO_CACHE);
    }

}
